<?php
namespace Common\Lib;
/**
 * 身份证严格验证代码
 * User: yezhimeih7
 * Date: 2017/4/5
 * Time: 22:08
 * @copy www.tcioc.com
 *
 * 身份证号里面包含了多少的信息：
 *1、号码的结构
 *　公民身份号码是特征组合码，由十七位数字本体码和一位校验码组成。排列顺序从左至右依次为：六位数字地址码，八位数字出生日期码，三位数字顺序码和一位数字校验码。
 *2、地址码(前六位数）
 *　表示编码对象常住户口所在县(市、旗、区)的行政区划代码，按gb/T2260的规定执行。
 *3、出生日期码（第七位至十四位）
 *　表示编码对象出生的年、月、日，按gb/T7408的规定执行，年、月、日代码之间不用分隔符。
 *4、顺序码（第十五位至十七位）
 *　表示在同一地址码所标识的区域范围内，对同年、同月、同日出生的人编定的顺序号，顺序码的奇数分配给男性，偶数分配给女性。
 *5、校验码（第十八位数）
 *　作为尾号的校验码，是由号码编制单位按统一的公式计算出来的，如果某人的尾号是0－9，都不会出现X，但如果尾号是10，那么就得用X来代替，因为如果用10做尾号，那么 此人的身份证就变成了19位。X是罗马数字的10，用X来代替10，可以保证公民的身份证符合国家标准。
 *
 * 用法一：<br>
 * $ch=new IDCardCheck("511323199510022369"); <br>
 * $ch->checked();  返回值 true为通过，返回值 false为不通过
 *
 * 用法二：<br>
 * $ch=new IDCardCheck();
 * $ch->idCard="511323199510022369";
 * $ch->checked();  返回值 true为通过，返回值 false为不通过
 */
class IDCardCheck{
    private $cities=array(
        '11'=>'北京',
        '12'=>'天津',
        '13'=>'河北',
        '14'=>'山西',
        '15'=>'内蒙古',
        '21'=>'辽宁',
        '22'=>'吉林',
        '23'=>'黑龙江',
        '31'=>'上海',
        '32'=>'江苏',
        '33'=>'浙江',
        '34'=>'安徽',
        '35'=>'福建',
        '36'=>'江西',
        '37'=>'山东',
        '41'=>'河南',
        '42'=>'湖北',
        '43'=>'湖南',
        '44'=>'广东',
        '45'=>'广西',
        '46'=>'海南',
        '50'=>'重庆',
        '51'=>'四川',
        '52'=>'贵州',
        '53'=>'云南',
        '54'=>'西藏',
        '61'=>'陕西',
        '62'=>'甘肃',
        '63'=>'青海',
        '64'=>'宁夏',
        '65'=>'新疆',
        '71'=>'台湾',
        '81'=>'香港',
        '82'=>'澳门',
        '91'=>'国外',
    );
    public $idCard="";

    public function __construct($idCard="")
    {
        if(!empty($idCard)){
            $this->idCard=$idCard;
        }
    }

    /**
     * 检测身份证是否合法
     * @return bool 返回值 true为通过，返回值 false为不通过
     */
    public function checked(){
        //判断是否为空
        if(empty($this->idCard)){
            return false;
        }
        //检验长度，类型
        if(!$this->isCardNo()){
            return false;
        }
        //检查省份
        if(!$this->checkProvince()){
            return false;
        }
        //校验生日
        if(!$this->checkBirthday()){
            return false;
        }
        //检验位的检测
        if(!$this->checkParity()){
            return false;
        }
        return true;
    }

    /**
     * 身份证号码为15位或者18位，15位时全为数字，18位前17位为数字，最后一位是校验位，可能为数字或字符X
     * @return boolean true为通过，false为验证不通过
     */
    private function isCardNo(){
        $reg='/(^\d{15}$)|(^\d{17}(\d|X)$)/';
        return preg_match($reg,$this->idCard);
    }

    /**
     * 取身份证前两位,校验省份
     * @return bool true为验证通过，false为验证不通过
     */
    private  function checkProvince(){
        $province=substr($this->idCard,0,2);
        $vcity=$this->cities;
        if(isset($vcity[$province])){
            return true;
        }
        return false;
    }

    private function checkBirthday(){
        $len=strlen($this->idCard);
        $arr_data=array();
        //身份证15位时，次序为省（3位）市（3位）年（2位）月（2位）日（2位）校验位（3位），皆为数字
        if($len==15){
            $re_fifteen='/^(\d{6})(\d{2})(\d{2})(\d{2})(\d{3})$/';
            preg_match($re_fifteen,$this->idCard,$arr_data);
            $year=$arr_data[2];
            $month=$arr_data[3];
            $day=$arr_data[4];
            $birthday='19'.$year."/".$month."/".$day;
            return $this->verifyBirthday("19".$year,$month,$day,$birthday);
        }
        //身份证18位时，次序为省（3位）市（3位）年（4位）月（2位）日（2位）校验位（4位），校验位末尾可能为X
        if($len==18){
            $re_fifteen='/^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$/';
            preg_match($re_fifteen,$this->idCard,$arr_data);
            $year=$arr_data[2];
            $month=$arr_data[3];
            $day=$arr_data[4];
            $birthday=$year."/".$month."/".$day;
            return $this->verifyBirthday($year,$month,$day,$birthday);
        }
        return false;
    }

    /**
     * 校验日期
     * @param $year
     * @param $month
     * @param $day
     * @param $birthday
     * @return bool
     */
    private function verifyBirthday($year,$month,$day,$birthday){
        $now=time();
        $now_year=date("Y",$now);
        $time=intval($now_year)-intval($year);
        $birthday_time=strtotime($birthday);
        if(date("Y/m/d",$birthday_time)!==$birthday){
            return false;
        }
        if(date("Y",$birthday_time)!==$year){
            return false;
        }
        if(date("m",$birthday_time)!==$month){
            return false;
        }
        if(date("d",$birthday_time)!==$day){
            return false;
        }

        if($time>=0&& $time<=130){
            return true;
        }
        return false;
    }

    /**
     * 校验位的检测
     * @return bool
     */
    private function checkParity(){
        //15位转18位
        $this->idCard=$this->changeFivteenToEighteen();
        $len=strlen($this->idCard);
        if($len==18){
            $arrInt=array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
            $arrCh=array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
            $cardTemp=0;
            for($i=0;$i<17;$i++){
                $cardTemp += (int) substr($this->idCard,$i,1)*$arrInt[$i];
            }
            $valnum=$arrCh[$cardTemp%11];
            if($valnum==substr($this->idCard,17,1)){
                return true;
            }
            return false;
        }
        return false;
    }

    /**
     * 15位转18位身份证号
     * @return string
     */
    private function changeFivteenToEighteen(){
        if(strlen($this->idCard)==15){
            $arrInt=array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
            $arrCh=array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
            $cardTemp=0;
            $this->idCard=substr($this->idCard,0,6)."19".substr($this->idCard,6,9);
            for($i=0;$i<17;$i++){
                $cardTemp+=(int)substr($this->idCard,$i,1)*$arrInt[$i];
            }
            $this->idCard.=$arrCh[$cardTemp%11];
            return $this->idCard;
        }
        return $this->idCard;
    }
}
